﻿/*
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.

Copyright (C) 2005-2014  Sylvain Rougeaux (Cyber Sinh) (http://www.luminescence-software.org/)
*/

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Text;

namespace Luminescence.Xiph
{
   /// <summary>
   ///   Classe abstraite assurant la gestion des tags Vorbis Comment.
   /// </summary>
   public abstract class VorbisComment
   {
      private readonly Dictionary<string, List<string>> tags = new Dictionary<string, List<string>>();
      private readonly Collection<ID3PictureFrame> arts = new Collection<ID3PictureFrame>();

      private const string PictureTag = "METADATA_BLOCK_PICTURE";

      /// <summary>
      ///   Liste des tags Vorbis officiels.
      /// </summary>
      public static readonly ReadOnlyCollection<string> OfficialVorbisTags = new ReadOnlyCollection<string>(new List<string>
            {
               "ARTIST",
               "TITLE",
               "DATE",
               "ALBUM",
               "GENRE",
               "TRACKNUMBER",
               "CONTACT",
               "COPYRIGHT",
               "DESCRIPTION",
               "ISRC",
               "LICENSE",
               "LOCATION",
               "ORGANIZATION",
               "PERFORMER",
               "VERSION"
            });

      /// <summary>
      ///   Méthode d'enregistrement des tags.
      /// </summary>
      public abstract void SaveMetadata();

      /// <summary>
      ///   Méthode de lecture d'un fichier contenant des métadonnées au format Vorbis Comment.
      /// </summary>
      /// <param name = "fileName">Chemin d'accès du fichier à analyser</param>
      public abstract void ReadMetadata(string fileName);

      #region Tags Vorbis Comment

      /// <summary>
      ///   Obtient ou définit le tag "Titre".
      /// </summary>
      public string Title
      {
         get { return tags.ContainsKey("TITLE") ? tags["TITLE"][0] : null; }
         set
         {
            tags.Remove("TITLE");
            if (value != null) tags.AddTag("TITLE", value);
         }
      }

      /// <summary>
      ///   Obtient ou définit le tag "Version".
      /// </summary>
      public string Version
      {
         get { return tags.ContainsKey("VERSION") ? tags["VERSION"][0] : null; }
         set
         {
            tags.Remove("VERSION");
            if (value != null) tags.AddTag("VERSION", value);
         }
      }

      /// <summary>
      ///   Obtient ou définit le tag "Album".
      /// </summary>
      public string Album
      {
         get { return tags.ContainsKey("ALBUM") ? tags["ALBUM"][0] : null; }
         set
         {
            tags.Remove("ALBUM");
            if (value != null) tags.AddTag("ALBUM", value);
         }
      }

      /// <summary>
      ///   Obtient ou définit le tag "Numéro de piste".
      /// </summary>
      public string TrackNumber
      {
         get { return tags.ContainsKey("TRACKNUMBER") ? tags["TRACKNUMBER"][0] : null; }
         set
         {
            tags.Remove("TRACKNUMBER");
            if (value != null) tags.AddTag("TRACKNUMBER", value);
         }
      }

      /// <summary>
      ///   Obtient ou définit le tag "Artiste".
      /// </summary>
      public string Artist
      {
         get { return tags.ContainsKey("ARTIST") ? tags["ARTIST"][0] : null; }
         set
         {
            tags.Remove("ARTIST");
            if (value != null) tags.AddTag("ARTIST", value);
         }
      }

      /// <summary>
      ///   Obtient ou définit le tag "Compositeur".
      /// </summary>
      public string Performer
      {
         get { return tags.ContainsKey("PERFORMER") ? tags["PERFORMER"][0] : null; }
         set
         {
            tags.Remove("PERFORMER");
            if (value != null) tags.AddTag("PERFORMER", value);
         }
      }

      /// <summary>
      ///   Obtient ou définit le tag "Copyright".
      /// </summary>
      public string Copyright
      {
         get { return tags.ContainsKey("COPYRIGHT") ? tags["COPYRIGHT"][0] : null; }
         set
         {
            tags.Remove("COPYRIGHT");
            if (value != null) tags.AddTag("COPYRIGHT", value);
         }
      }

      /// <summary>
      ///   Obtient ou définit le tag "License".
      /// </summary>
      public string License
      {
         get { return tags.ContainsKey("LICENSE") ? tags["LICENSE"][0] : null; }
         set
         {
            tags.Remove("LICENSE");
            if (value != null) tags.AddTag("LICENSE", value);
         }
      }

      /// <summary>
      ///   Obtient ou définit le tag "Organisation".
      /// </summary>
      public string Organization
      {
         get { return tags.ContainsKey("ORGANIZATION") ? tags["ORGANIZATION"][0] : null; }
         set
         {
            tags.Remove("ORGANIZATION");
            if (value != null) tags.AddTag("ORGANIZATION", value);
         }
      }

      /// <summary>
      ///   Obtient ou définit le tag "Description".
      /// </summary>
      public string Description
      {
         get { return tags.ContainsKey("DESCRIPTION") ? tags["DESCRIPTION"][0] : null; }
         set
         {
            tags.Remove("DESCRIPTION");
            if (value != null) tags.AddTag("DESCRIPTION", value);
         }
      }

      /// <summary>
      ///   Obtient ou définit le tag "Genre".
      /// </summary>
      public string Genre
      {
         get { return tags.ContainsKey("GENRE") ? tags["GENRE"][0] : null; }
         set
         {
            tags.Remove("GENRE");
            if (value != null) tags.AddTag("GENRE", value);
         }
      }

      /// <summary>
      ///   Obtient ou définit le tag "Date".
      /// </summary>
      public string Date
      {
         get { return tags.ContainsKey("DATE") ? tags["DATE"][0] : null; }
         set
         {
            tags.Remove("DATE");
            if (value != null) tags.AddTag("DATE", value);
         }
      }

      /// <summary>
      ///   Obtient ou définit le tag "Localisation".
      /// </summary>
      public string Location
      {
         get { return tags.ContainsKey("LOCATION") ? tags["LOCATION"][0] : null; }
         set
         {
            tags.Remove("LOCATION");
            if (value != null) tags.AddTag("LOCATION", value);
         }
      }

      /// <summary>
      ///   Obtient ou définit le tag "Contact".
      /// </summary>
      public string Contact
      {
         get { return tags.ContainsKey("CONTACT") ? tags["CONTACT"][0] : null; }
         set
         {
            tags.Remove("CONTACT");
            if (value != null) tags.AddTag("CONTACT", value);
         }
      }

      /// <summary>
      ///   Obtient ou définit le tag "ISRC".
      /// </summary>
      public string ISRC
      {
         get { return tags.ContainsKey("ISRC") ? tags["ISRC"][0] : null; }
         set
         {
            tags.Remove("ISRC");
            if (value != null) tags.AddTag("ISRC", value);
         }
      }

      #endregion

      /// <summary>
      ///   Retourne le "Vendor".
      /// </summary>
      public string Vendor { get; private set; }

      /// <summary>
      ///   Supprime tous les tags non officiels.
      /// </summary>
      public void ClearNonOfficialTags()
      {
         foreach (string key in tags.Where(kvp => !OfficialVorbisTags.Contains(kvp.Key)).Select(kvp => kvp.Key).ToList())
            tags.Remove(key);
      }

      /// <summary>
      ///   Supprime tous les tags.
      /// </summary>
      public void ClearTags() => tags.Clear();

      /// <summary>
      ///   Supprime toutes les images.
      /// </summary>
      public void ClearArts() => arts.Clear();

      /// <summary>
      ///   Ajoute une image.
      /// </summary>
      public void AddArt(ID3PictureFrame pictureFrame)
      {
         if (pictureFrame != null && !arts.Contains(pictureFrame))
            arts.Add(pictureFrame);
      }

      /// <summary>
      ///   Supprime une image.
      /// </summary>
      public bool RemoveArt(ID3PictureFrame pictureFrame) => arts.Remove(pictureFrame);

      /// <summary>
      ///   Collection en lecture seule contenant les images.
      /// </summary>
      public ReadOnlyCollection<ID3PictureFrame> Arts => new ReadOnlyCollection<ID3PictureFrame>(arts);

      /// <summary>
      ///   Retourne le nombre total de tags.
      /// </summary>
      public int TagsCount => tags.Sum(kvp => kvp.Value.Count);

      /// <summary>
      ///   Renvoie une collection en lecture seule des tags de même nom.
      /// </summary>
      /// <param name = "key">Nom du tag</param>
      /// <returns>Collection en lecture seule contenant les valeurs du tag demandé</returns>
      public ReadOnlyCollection<string> this[string key]
      {
         get
         {
            key = CheckedVorbisCommentKey(key);
            return tags.ContainsKey(key) ? tags[key].AsReadOnly() : null;
         }
      }

      /// <summary>
      ///   Ajoute un nouveau tag.
      /// </summary>
      /// <param name = "name">Nom du tag</param>
      /// <param name = "content">Valeur du tag</param>
      public void AddTag(string name, string content) => tags.AddTag(name, content);

      /// <summary>
      ///   Supprime un tag.
      /// </summary>
      /// <param name = "name">Nom du tag</param>
      /// <returns>True si la suppression du tag réussi, sinon False</returns>
      public bool RemoveTag(string name) => tags.Remove(CheckedVorbisCommentKey(name));

      /// <summary>
      ///   Supprime un tag.
      /// </summary>
      /// <param name = "name">Nom du tag</param>
      /// <param name = "index">Index du tag</param>
      /// <returns>True si la suppression du tag à l'index spécifié réussi, sinon False</returns>
      public bool RemoveTag(string name, int index)
      {
         name = CheckedVorbisCommentKey(name);
         if (tags.ContainsKey(name) && index >= 0 && index < tags[name].Count)
         {
            tags[name].RemoveAt(index);
            return true;
         }

         return false;
      }

      /// <summary>
      ///   Modifie un tag.
      /// </summary>
      /// <param name = "name">Nom du tag</param>
      /// <param name = "content">Valeur du nouveau tag</param>
      /// <returns>True si la modification du tag réussi, sinon False</returns>
      public bool SetTag(string name, string content)
      {
         name = CheckedVorbisCommentKey(name);
         if (tags.ContainsKey(name))
         {
            tags[name] = new List<string>(new[] { content.Trim() });
            return true;
         }

         return false;
      }

      /// <summary>
      ///   Modifie un tag.
      /// </summary>
      /// <param name = "name">Nom du tag</param>
      /// <param name = "content">Valeur du nouveau tag</param>
      /// <param name = "index">Index du tag</param>
      /// <returns>True si la modification du tag à l'index spécifié réussi, sinon False</returns>
      public bool SetTag(string name, string content, int index)
      {
         name = CheckedVorbisCommentKey(name);
         if (tags.ContainsKey(name) && index >= 0 && index < tags[name].Count)
         {
            tags[name][index] = content.Trim();
            return true;
         }

         return false;
      }

      /// <summary>
      ///   Retourne une collection contenant tous les tags.
      /// </summary>
      /// <returns>Collection contenant les noms et valeurs de tous les tags</returns>
      public Dictionary<string, List<string>> GetAllTags() => new Dictionary<string, List<string>>(tags);

      /// <summary>
      ///   Lit des tags au format Vorbis Comment dans un flux.
      /// </summary>
      /// <param name = "stream">Flux à partir duquel les tags doivent être lus</param>
      /// <param name = "readFramingBit">Vrai si le "framing bit" doit être lu</param>
      public void ReadVorbisComment(Stream stream, bool readFramingBit)
      {
         var br = new BinaryReader(stream);
         int vendor_length = br.ReadInt32();
         byte[] buffer = br.ReadBytes(vendor_length);
         var utf8 = new UTF8Encoding();
         Vendor = utf8.GetString(buffer);

         int comment_number = br.ReadInt32();
         for (int i = 0; i < comment_number; i++)
         {
            int length = br.ReadInt32();
            buffer = br.ReadBytes(length);
            string comment2 = utf8.GetString(buffer);
            int sepindex = comment2.IndexOf('=');

            if (sepindex == -1)
               throw new FileFormatException("The Vorbis Comment contains a malformed tag.");

            string name = comment2.Substring(0, sepindex);
            string value = comment2.Substring(sepindex + 1);

            if (name == PictureTag)
            {
               try
               {
                  byte[] data = Convert.FromBase64String(value);
                  arts.Add(new ID3PictureFrame(data));
               }
               catch (FormatException)
               {
                  throw new FileFormatException("The Vorbis Comment contains an invalid picture tag (malformed Base64 data).");
               }
            }
            else
               tags.AddTag(name, value);
         }

         if (readFramingBit)
         {
            if (br.ReadByte() != 1)
               throw new FileFormatException("The framing bit is unset in Vorbis Comment block.");
         }
      }

      /// <summary>
      ///   Ecrit des tags au format Vorbis Comment à partir d'un dictionnaire les contenant.
      /// </summary>
      /// <param name = "serializeArts">Vrai si les images doivent être encodées en base64 au format Vorbis Comment</param>
      /// <param name = "writeFramingBit">Vrai si le "framing bit" doit être ajouté à la fin du block Vorbis Comment</param>
      /// <returns>Tableau d'octets contenant les tags au fomat Vorbis Comment</returns>
      public byte[] WriteVorbisComment(bool serializeArts, bool writeFramingBit)
      {
         var vorbisTags = new List<string>();
         foreach (KeyValuePair<string, List<string>> kvp in tags)
            vorbisTags.AddRange(kvp.Value.Select(s => kvp.Key + "=" + s));

         if (serializeArts)
         {
            foreach (ID3PictureFrame art in arts)
               vorbisTags.Add(PictureTag + "=" + Convert.ToBase64String(art.ToApicFrame()));
         }

         using (var ms = new MemoryStream())
         {
            var bw = new BinaryWriter(ms);
            var utf8 = new UTF8Encoding();
            byte[] buffer = utf8.GetBytes(Vendor);
            bw.Write(buffer.Length);
            bw.Write(buffer);
            bw.Write(vorbisTags.Count);
            foreach (string t in vorbisTags)
            {
               buffer = utf8.GetBytes(t);
               bw.Write(buffer.Length);
               bw.Write(buffer);
            }

            if (writeFramingBit)
               bw.Write((byte)1);

            return ms.ToArray(); 
         }
      }

      /// <summary>
      ///   S'assure de la validité des clefs utilisé dans les tags Vorbis Comment.
      /// </summary>
      /// <param name = "key">Clef à valider</param>
      /// <returns>Valeur du tag en majuscule</returns>
      /// <remarks>
      ///   Déclenche une exception en cas de présence d'un caractère non valide dans le nom du tag
      /// </remarks>
      public static string CheckedVorbisCommentKey(string key)
      {
         foreach (char c in key)
         {
            if (c < 30 || c > 125 || c == '=')
               throw new FormatException("The Vorbis Comment key contains this invalid character: " + c);
         }

         return key.ToUpperInvariant();
      }
   }
}